Этапы реализации проекта в ПЛИС

Для того, чтобы описанное на языке описания аппаратуры устройство было реализовано в ПЛИС, необходимо выполнить несколько этапов:

  1. Предобработку (elaboration)
  2. Синтез (synthesis)
  3. Внедрение (implementation)
  4. Создание двоичного файла конфигурации (generate bitstream)

Остановимся на каждом шаге подробнее:

Предобработка

На этапе предобработки, САПР считывает HDL-описание вашего устройства, производит подстановку значений параметров и блоков generate, устанавливает разрядности сигналов и строит иерархию модулей устройства. Затем, ставит в соответствие отдельным участкам кода соответствующие абстрактные элементы: логические вентили, мультиплексоры, элементы памяти и т.п. Кроме того, производится анализ и оптимизация схемы, например, если какая-то часть логики в конечном итоге не связана ни с одним из выходных сигналов, эта часть логики будет удалена[1]. Итогом предобработки является так называемая топология межсоединений (в быту называемая словом нетлист). Нетлист — это представление цифровой схемы в виде графа, где каждый элемент схемы является узлом, а цепи, соединяющие эти элементы являются ребрами. Нетлист может храниться как в виде каких-то внутренних файлов САПР-а (так хранится нетлист этапа предобработки), так и в виде HDL-файла, описывающего экземпляры примитивов и связи между ними. Рассмотрим этап предобработки и термин нетлиста на примере.

Допустим, мы хотим реализовать следующую цифровую схему:

../.pic/Introduction/Implementation%20steps/fig_01.drawio.svg

Её можно описать следующим SystemVerilog-кодом:

module sample(
    input logic a, b, c, d, sel,
    output logic res
    );

    logic ab = a & b;
    logic xabc = ab ^ c;

    assign res = sel? d : xabc;

endmodule

Выполним этап элаборации. Для этого в Vivado на вкладке RTL Analysis выберем Schematic.

Откроются следующие окна:

../.pic/Introduction/Implementation%20steps/fig_02.png

В левом окне мы видим наш нетлист. В нижней части обозначены узлы графа (элементы ab_i, res_i, xabc_i, которые представляют собой И, мультиплексор и Исключающее ИЛИ соответственно. Имена этих элементов схожи с именами проводов, присваиванием которым мы создавали данные элементы)

В верхней части обозначены ребра графа, соединяющие элементы схемы. Это входы и выходы нашего модуля, а также созданные нами промежуточные цепи.

Справа вы видите схематикграфическую схему, построенную на основе данного графа (нетлиста).

Synthesis

На шаге синтеза, САПР берет сгенерированную ранее цифровую схему и реализует элементы этой схемы через примитивы конкретной ПЛИС — в основном через логические ячейки, содержащие таблицы подстановки, полный однобитный сумматор и D-триггер (см. как работает ПЛИС).

В рамках нашего примера, САПР смотрит на построенный на этапе элаборации нетлист и решает, какими средствами (примитивами) ПЛИС можно его реализовать. Поскольку схема чисто комбинационная, результат её работы можно рассчитать и выразить в виде таблицы истинности, а значит для её реализации лучше всего подойдут Таблицы Подстановки (LUT-ы). Более того, в ПЛИС xc7a100tcsg324-1 есть пятивходовые LUT-ы, а у нашей схемы именно столько входов. Это означает, работу всей этой схемы можно заменить всего одной таблицей подстановки внутри ПЛИС.

Итак, продолжим рассматривать наш пример и выполним этап синтеза. Для этого нажмем на кнопку Run Synthesis.

После выполнения синтеза у нас появится возможность открыть новый схематик, сделаем это.

../.pic/Introduction/Implementation%20steps/fig_03.png

Мы видим, что между входами/выходами схемы и её внутренней логикой появились новые примитивы — буферы. Они нужны, преобразовать уровень напряжения между ножками ПЛИС и внутренней логикой (условно говоря, на вход плис могут приходить сигналы с уровнем 3.3 В, а внутри ПЛИС примитивы работают с сигналами уровня 1.2 В).

Сама же логика, как мы и предполагали, свернулась в одну пятивходовую таблицу подстановки.

строка EQN=32'hAAAA3CCC означает, что таблица подстановки будет инициализирована следующим 32-битным значением: 0xAAAA3CCC. Поскольку у схемы 5 входов, у нас может быть 25=32 возможных комбинаций входных сигналов и для каждого нужно свое значение результата.

Можно ли как-то проверить данное 32-битное значение без просчитывания всех 32 комбинаций сигналов "на бумажке"?

Да, можно. Сперва надо понять в каком именно порядке будут идти эти комбинации. Мы видим, что сигналы подключены к таблице подстановки в следующем порядке: `d, c, b, a, sel`. Это означает, что сама таблица примет вид:
|sel| a | b | c | d |  |res|
|---|---|---|---|---|  |---|
| 0 | 0 | 0 | 0 | 0 |  | 0 |
| 0 | 0 | 0 | 0 | 1 |  | 0 |
| 0 | 0 | 0 | 1 | 0 |  | 1 |
| 0 | 0 | 0 | 1 | 1 |  | 1 |
| 0 | 0 | 1 | 0 | 0 |  | 0 |
....
| 1 | 1 | 1 | 1 | 1 |  | 1 |

Давайте посмотрим на логику исходной схемы и данную таблицу истинности: когда sel==1, на выход идет d, это означает, что мы знаем все значения для нижней половины таблицы истинности, в нижней половине таблице истинности самый левый входной сигнал (sel) равен только единице, значит результат будет совпадать с сигналом d, который непрерывно меняется с 0 на 1 и оканчивается значением 1. Это означает, что если читать значения результатов снизу-вверх (от старших значений к младшим), то сначала у нас будет 16 цифр, представляющих 8 пар 10:101010101010, что эквивалентно записи AAAA в шестнадцатеричной записи.

Рассчитывать оставшиеся 16 вариантов тоже не обязательно, если посмотреть на схему, то можно заметить, что когда sel!=1, ни sel, ни d больше не участвуют в управлении выходом. Выход будет зависеть от операции xor, которая дает 1 только когда её входы не равны между собой. Верхний вход xor (выход И) , будет равен единице только когда входы a и b равны единице, причем в данный момент, нас интересуют только ситуации, когда sel!=1. Принимая во внимание, что в таблице истинности значения входов записываются чередующимися степенями двойки (единицами, парами, четверками, восьмерками) единиц и нулей, мы понимаем, что интересующая нас часть таблицы истинности будет выглядеть следующим образом:

       ...

  | a | b | c |
. |---|---|---| .
. | 1 | 1 | 0 | .
. | 1 | 1 | 0 | .
  | 1 | 1 | 1 |
  | 1 | 1 | 1 |

       ...

Только в этой части таблицы истинности мы получим 1 на выходе И, при этом в старшей части, вход c так же равен 1. Это значит, что входы Исключающего ИЛИ будут равны и на выходе будет 0. Значит результат этой таблицы истинности будет равен 0011 или 3 в шестнадцатеричной записи.

Ниже данной части таблицы истинности располагается та часть, где sel==1, выше та часть, где выход И будет равен 0. Это означает, что оставшаяся младшая часть будет повторять значения c, которое сменяется парами нулей и единиц: 00-11-00-11... Эта оставшаяся последовательность будет записана в шестнадцатеричном виде как 0xCCC.

Таким образом, мы и получаем искомое выражение EQN=32'hAAAA3CCC. Если с этой частью возникли сложности, попробуйте составить данную таблицу истинности (без вычисления самих результатов, а затем просмотрите логику быстрого вычисления результата).

Implementation

После построения новой схемы, где в качестве элементов используются ресурсы конкретной ПЛИС, происходит расчёт размещения этой схемы внутри ПЛИС: выбираются конкретные логические ячейки, происходит маршрутизация сигнала между этими ячейками. Например, реализация 32-битного сумматора с ускоренным переносом может потребовать 32 LUT-а и 8 примитивов вычисления быстрого переноса (CARRY4). Будет неразумно использовать для этого примитивы, разбросанные по всему кристаллу ПЛИС, ведь тогда придется выполнять сложную маршрутизацию сигнала, да и временные характеристики устройства так же пострадают (сигналу идущему от предыдущего разряда к следующему придется проходить больший путь). Вместо этого, САПР будет пытаться разместить схему таким образом, чтобы использовались близлежащие примитивы ПЛИС, для получения оптимальных характеристик.

Что именно считается "оптимальным" зависит от двух вещей: настроек САПР и ограничений (constraints), наложенных на имплементацию. Ограничения сужают область возможных решений по размещению примитивов внутри ПЛИС под определенные характеристики (временны́е и физические). Например, можно сказать, внутри ПЛИС схема должна быть размещена таким образом, чтобы время прохождения по критическому пути не превышало 20ns. Это временно́е ограничение. Также нужно сообщить САПР, к какой ножке ПЛИС необходимо подключить входы и выходы нашей схемы — это физическое ограничение.

Ограничения описываются не на языке описания аппаратуры, вместо этого используются текстовые файлы специального формата, зависящего от конкретной САПР.

Пример используемых ограничений для лабораторной по АЛУ.

Строки, начинающиеся с # являются комментариями.

Строки, начинающиеся с set_property являются физическими ограничениями, связывающими входы и выходы нашей схемы с конкретными входами и выходами ПЛИС.

Строка create_clock... задает временны́е ограничения, описывая целевую тактовую частоту тактового сигнала и его скважность.

## This file is a general .xdc for the Nexys A7-100T

# Clock signal

set_property -dict { PACKAGE_PIN E3    IOSTANDARD LVCMOS33 } [get_ports { CLK100 }]; #IO_L12P_T1_MRCC_35 Sch=clk100mhz
create_clock -add -name sys_clk_pin -period 10.00 -waveform {0 5} [get_ports {CLK100}];

# Switches
set_property -dict { PACKAGE_PIN J15   IOSTANDARD LVCMOS33 } [get_ports { SW[0] }]; #IO_L24N_T3_RS0_15 Sch=sw[0]
set_property -dict { PACKAGE_PIN L16   IOSTANDARD LVCMOS33 } [get_ports { SW[1] }]; #IO_L3N_T0_DQS_EMCCLK_14 Sch=sw[1]
set_property -dict { PACKAGE_PIN M13   IOSTANDARD LVCMOS33 } [get_ports { SW[2] }]; #IO_L6N_T0_D08_VREF_14 Sch=sw[2]
set_property -dict { PACKAGE_PIN R15   IOSTANDARD LVCMOS33 } [get_ports { SW[3] }]; #IO_L13N_T2_MRCC_14 Sch=sw[3]
set_property -dict { PACKAGE_PIN R17   IOSTANDARD LVCMOS33 } [get_ports { SW[4] }]; #IO_L12N_T1_MRCC_14 Sch=sw[4]
set_property -dict { PACKAGE_PIN T18   IOSTANDARD LVCMOS33 } [get_ports { SW[5] }]; #IO_L7N_T1_D10_14 Sch=sw[5]
set_property -dict { PACKAGE_PIN U18   IOSTANDARD LVCMOS33 } [get_ports { SW[6] }]; #IO_L17N_T2_A13_D29_14 Sch=sw[6]
set_property -dict { PACKAGE_PIN R13   IOSTANDARD LVCMOS33 } [get_ports { SW[7] }]; #IO_L5N_T0_D07_14 Sch=sw[7]
set_property -dict { PACKAGE_PIN T8    IOSTANDARD LVCMOS18 } [get_ports { SW[8] }]; #IO_L24N_T3_34 Sch=sw[8]
set_property -dict { PACKAGE_PIN U8    IOSTANDARD LVCMOS18 } [get_ports { SW[9] }]; #IO_25_34 Sch=sw[9]
set_property -dict { PACKAGE_PIN R16   IOSTANDARD LVCMOS33 } [get_ports { SW[10] }]; #IO_L15P_T2_DQS_RDWR_B_14 Sch=sw[10]
set_property -dict { PACKAGE_PIN T13   IOSTANDARD LVCMOS33 } [get_ports { SW[11] }]; #IO_L23P_T3_A03_D19_14 Sch=sw[11]
set_property -dict { PACKAGE_PIN H6    IOSTANDARD LVCMOS33 } [get_ports { SW[12] }]; #IO_L24P_T3_35 Sch=sw[12]
set_property -dict { PACKAGE_PIN U12   IOSTANDARD LVCMOS33 } [get_ports { SW[13] }]; #IO_L20P_T3_A08_D24_14 Sch=sw[13]
set_property -dict { PACKAGE_PIN U11   IOSTANDARD LVCMOS33 } [get_ports { SW[14] }]; #IO_L19N_T3_A09_D25_VREF_14 Sch=sw[14]
set_property -dict { PACKAGE_PIN V10   IOSTANDARD LVCMOS33 } [get_ports { SW[15] }]; #IO_L21P_T3_DQS_14 Sch=sw[15]

### LEDs

set_property -dict { PACKAGE_PIN H17   IOSTANDARD LVCMOS33 } [get_ports { LED[0] }]; #IO_L18P_T2_A24_15 Sch=led[0]
set_property -dict { PACKAGE_PIN K15   IOSTANDARD LVCMOS33 } [get_ports { LED[1] }]; #IO_L24P_T3_RS1_15 Sch=led[1]
set_property -dict { PACKAGE_PIN J13   IOSTANDARD LVCMOS33 } [get_ports { LED[2] }]; #IO_L17N_T2_A25_15 Sch=led[2]
set_property -dict { PACKAGE_PIN N14   IOSTANDARD LVCMOS33 } [get_ports { LED[3] }]; #IO_L8P_T1_D11_14 Sch=led[3]
set_property -dict { PACKAGE_PIN R18   IOSTANDARD LVCMOS33 } [get_ports { LED[4] }]; #IO_L7P_T1_D09_14 Sch=led[4]
set_property -dict { PACKAGE_PIN V17   IOSTANDARD LVCMOS33 } [get_ports { LED[5] }]; #IO_L18N_T2_A11_D27_14 Sch=led[5]
set_property -dict { PACKAGE_PIN U17   IOSTANDARD LVCMOS33 } [get_ports { LED[6] }]; #IO_L17P_T2_A14_D30_14 Sch=led[6]
set_property -dict { PACKAGE_PIN U16   IOSTANDARD LVCMOS33 } [get_ports { LED[7] }]; #IO_L18P_T2_A12_D28_14 Sch=led[7]
set_property -dict { PACKAGE_PIN V16   IOSTANDARD LVCMOS33 } [get_ports { LED[8] }]; #IO_L16N_T2_A15_D31_14 Sch=led[8]
set_property -dict { PACKAGE_PIN T15   IOSTANDARD LVCMOS33 } [get_ports { LED[9] }]; #IO_L14N_T2_SRCC_14 Sch=led[9]
set_property -dict { PACKAGE_PIN U14   IOSTANDARD LVCMOS33 } [get_ports { LED[10] }]; #IO_L22P_T3_A05_D21_14 Sch=led[10]
set_property -dict { PACKAGE_PIN T16   IOSTANDARD LVCMOS33 } [get_ports { LED[11] }]; #IO_L15N_T2_DQS_DOUT_CSO_B_14 Sch=led[11]
set_property -dict { PACKAGE_PIN V15   IOSTANDARD LVCMOS33 } [get_ports { LED[12] }]; #IO_L16P_T2_CSI_B_14 Sch=led[12]
set_property -dict { PACKAGE_PIN V14   IOSTANDARD LVCMOS33 } [get_ports { LED[13] }]; #IO_L22N_T3_A04_D20_14 Sch=led[13]
set_property -dict { PACKAGE_PIN V12   IOSTANDARD LVCMOS33 } [get_ports { LED[14] }]; #IO_L20N_T3_A07_D23_14 Sch=led[14]
set_property -dict { PACKAGE_PIN V11   IOSTANDARD LVCMOS33 } [get_ports { LED[15] }]; #IO_L21N_T3_DQS_A06_D22_14 Sch=led[15]

## 7 segment display
set_property -dict { PACKAGE_PIN T10   IOSTANDARD LVCMOS33 } [get_ports { CA }]; #IO_L24N_T3_A00_D16_14 Sch=ca
set_property -dict { PACKAGE_PIN R10   IOSTANDARD LVCMOS33 } [get_ports { CB }]; #IO_25_14 Sch=cb
set_property -dict { PACKAGE_PIN K16   IOSTANDARD LVCMOS33 } [get_ports { CC }]; #IO_25_15 Sch=cc
set_property -dict { PACKAGE_PIN K13   IOSTANDARD LVCMOS33 } [get_ports { CD }]; #IO_L17P_T2_A26_15 Sch=cd
set_property -dict { PACKAGE_PIN P15   IOSTANDARD LVCMOS33 } [get_ports { CE }]; #IO_L13P_T2_MRCC_14 Sch=ce
set_property -dict { PACKAGE_PIN T11   IOSTANDARD LVCMOS33 } [get_ports { CF }]; #IO_L19P_T3_A10_D26_14 Sch=cf
set_property -dict { PACKAGE_PIN L18   IOSTANDARD LVCMOS33 } [get_ports { CG }]; #IO_L4P_T0_D04_14 Sch=cg
# set_property -dict { PACKAGE_PIN H15   IOSTANDARD LVCMOS33 } [get_ports { DP }]; #IO_L19N_T3_A21_VREF_15 Sch=dp
set_property -dict { PACKAGE_PIN J17   IOSTANDARD LVCMOS33 } [get_ports { AN[0] }]; #IO_L23P_T3_FOE_B_15 Sch=an[0]
set_property -dict { PACKAGE_PIN J18   IOSTANDARD LVCMOS33 } [get_ports { AN[1] }]; #IO_L23N_T3_FWE_B_15 Sch=an[1]
set_property -dict { PACKAGE_PIN T9    IOSTANDARD LVCMOS33 } [get_ports { AN[2] }]; #IO_L24P_T3_A01_D17_14 Sch=an[2]
set_property -dict { PACKAGE_PIN J14   IOSTANDARD LVCMOS33 } [get_ports { AN[3] }]; #IO_L19P_T3_A22_15 Sch=an[3]
set_property -dict { PACKAGE_PIN P14   IOSTANDARD LVCMOS33 } [get_ports { AN[4] }]; #IO_L8N_T1_D12_14 Sch=an[4]
set_property -dict { PACKAGE_PIN T14   IOSTANDARD LVCMOS33 } [get_ports { AN[5] }]; #IO_L14P_T2_SRCC_14 Sch=an[5]
set_property -dict { PACKAGE_PIN K2    IOSTANDARD LVCMOS33 } [get_ports { AN[6] }]; #IO_L23P_T3_35 Sch=an[6]
set_property -dict { PACKAGE_PIN U13   IOSTANDARD LVCMOS33 } [get_ports { AN[7] }]; #IO_L23N_T3_A02_D18_14 Sch=an[7]

## Buttons
set_property -dict { PACKAGE_PIN C12   IOSTANDARD LVCMOS33 } [get_ports { resetn }]; #IO_L3P_T0_DQS_AD1P_15 Sch=cpu_resetn

После выполнения имплементации, нетлист и схема остаются неизменными, однако использованные для реализации схемы примитивы получают свой "адрес" внутри ПЛИС:

cell_add../.pic/Introduction/Implementation%20steps/fig_04.png

Теперь, мы можем посмотреть на "внутренности" нашей ПЛИС xc7a100tcsg324-1 и то, как через её примитивы будет реализована наша схема. Для этого необходимо отрыть имплементированное устройство: Implementation -> Open implemented design. Откроется следующее окно:

../.pic/Introduction/Implementation%20steps/fig_05.png

Может показаться очень страшным и непонятным, но это содержимое ПЛИС. Просто из-за огромного количества содержащихся в ней примитивов, она показана в таком масштабе, что все сливается в один цветной ковер. Большая часть этого окна неактивна (показана в темно-синих тонах) и это нормально, ведь мы реализовали крошечную цифровую схему, она и не должна занимать значительное количество ресурсов ПЛИС.

Нас интересует "бледно-голубая точка", расположенная в нижнем левом углу прямоугольника X0Y1 (выделено красным). Если отмасштабировать эту зону, мы найдем используемый нами LUT:

../.pic/Introduction/Implementation%20steps/fig_06.png

Кроме того, если поиграться со свойствами этого примитива, мы сможем найти нашу таблицу истинности, инициализирующую этот примитив.

Generate Bitstream

После того, как САПР определил конкретные примитивы, их режим работы, и пути сигнала между ними, необходимо создать двоичный файл (bitstream), который позволит сконфигурировать ПЛИС необходимым нам образом. Получив этот файл, остается запрограммировать ПЛИС, после чего она воплотит разработанное устройство.

Выводы

Таким образом, маршрут перехода от HDL-описания устройства до его реализации в ПЛИС выглядит следующим образом:

  1. Сперва происходит анализ HDL-описания. В ходе этого анализа выявляются простейшие структуры: регистры, мультиплексоры, вычислительные блоки (сложения/умножения/сдвига и т.п.). Строится граф схемы, построенной с помощью этих структур (нетлист). Данный нетлист платформонезависим, т.е. не привязан к конкретной ПЛИС.
  2. После происходит этап синтеза нетлиста, полученного на предыдущем этапе в нетлист, использующий имеющиеся ресурсы конкретной ПЛИС. Все, использовавшиеся на предыдущем этапе структуры (регистры, мультиплексоры и прочие блоки) реализуются через примитивы ПЛИС (LUT-ы, D-триггеры, блоки сложения и т.п.).
  3. Затем происходит этап размещения схемы внутри ПЛИС: если на предыдущем этапе часть схемы была реализована через LUT, то на этом этапе решается какой именно LUT будет использован. Область допустимых решений по этому вопросу сужается путем наложения ограничений (constraints).
  4. После размещения остается только сгенерировать двоичный файл (bitstream), который во время прошивки сконфигурирует ПЛИС на реализацию нашей схемы.

Список использованной литературы

  1. Форум Xilinx: what exactly is 'elaborating' a design?